package cn.tedu.charging.cost.service.impl;

import cn.tedu.charging.common.pojo.param.ChargingProcessParam;
import cn.tedu.charging.common.pojo.vo.ChargingProcessVO;
import cn.tedu.charging.cost.dao.repository.ChargingDataESRepository;
import cn.tedu.charging.cost.dao.repository.CostRepository;
import cn.tedu.charging.cost.dao.repository.CostRuleCacheRepository;
import cn.tedu.charging.cost.pojo.ChargingData;
import cn.tedu.charging.cost.pojo.po.ChargingDataPO;
import cn.tedu.charging.cost.pojo.po.CostRulePO;
import cn.tedu.charging.cost.service.CostService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;

@Slf4j
@Service
public class CostServiceImpl implements CostService {

    @Autowired
    private CostRepository costRepository;

    /**
     * 全局的map 用来存储设备同步的充电数据
     * Key 订单号 value 充电数据
     */
    private Map<String, ChargingData> chargingDataMap = new HashMap<>();


    @Autowired
    private ChargingDataESRepository chargingDataESRepository;

    private  ChargingDataPO  getChargingDataPOByOrderNo(String orderNo,Integer userId,Integer gunId){
        //通过订单号获取充电数据
        Optional<ChargingDataPO> optional =
                chargingDataESRepository.findById(orderNo);
        if (optional.isPresent()) {
            //如果数据库有充电数据直接返回
            return optional.get();
        }
        //如果数据库没有充电数据初始化并且返回
        return initChargingData(orderNo, userId, gunId);
    }

    /**
     * 通过订单号从ES 查询充电数据
     * 解决 本地map的问题
     * @param orderNo
     * @return
     */
    private ChargingDataPO getChargingDataByOrderNoFromES(String orderNo,
                                                          Integer currentHour,
                                                          CostRulePO costRulePO,
                                                          Float chargingCapacity,
                                                          Integer userId,
                                                          Integer gunId){
        ChargingDataPO chargingData = getChargingDataPOByOrderNo(orderNo,userId,gunId);
        //第一次的价格计算 充电度数 * 每度电的价格 同时保存到chargingData
        log.debug("计算价格");
        //获取实际充电度数  当前的充电度数 - 上次充电度数
        Float realChargingCapacity =
                chargingData.getChargingCapacity(chargingCapacity);
        //计算价格 充电度数 * 每度电的价格
        BigDecimal cost = getCost(realChargingCapacity, costRulePO.getPowerFee());
        //保存度数 到 chargingData 为了下一次计算 <<实际充电度数>> 做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //保存 本次花费 到 chargingData 为了下一次计算 <<总花费>> 做准备
        chargingData.setTotalFee(cost);
        log.debug("同步数据，" +
                        "当前时间:{},计价规则名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,costRulePO.getName(),
                chargingCapacity,costRulePO.getPowerFee(),
                cost,chargingData.getTotalFee());
        Integer count = chargingData.incrCount();
        log.debug("第{}次同步数据，保存数据到ES:{}",count,chargingData);
        chargingDataESRepository.save(chargingData);
        log.debug("第{}次同步数据，保存数据到ES成功:{}",count,chargingData);
        return chargingData;
    }

    private  ChargingDataPO initChargingData(String orderNo, Integer userId, Integer gunId) {
        //创建充电数据对象
        ChargingDataPO chargingData = new ChargingDataPO();
        chargingData.setId(orderNo);
        chargingData.setOrderNo(orderNo);
        chargingData.setUserId(userId);
        chargingData.setGunId(gunId);
        return chargingData;
    }


    /**
     * 通过订单号查询充电数据
     * @param orderNo
     * @return
     */
    private  ChargingData getChargingDataByOrderNo(String orderNo){
        ChargingData chargingData = chargingDataMap.get(orderNo);
        if (chargingData == null) {
            //等于null 表示是第一次同步充电数据
            //创建充电数据对象
            chargingData = new ChargingData();
            chargingData.setOrderNo(orderNo);
            chargingData.setCount(1);
            //保存到map中 为下次同步做准备
            chargingDataMap.put(orderNo,chargingData);
        }else {
            //不等于null 表示不是第一次同步充电数据 可能是第二次 也可能是第N次
            //获取上次的同步次数
            Integer count = chargingData.getCount();
            //上次的次数 + 本次  = 总次数
            count = count + 1;
            //保存本次次数 为下次计算做准备
            chargingData.setCount(count);


            /*//获取上次的花费
            BigDecimal lastTotalFee = chargingData.getTotalFee();
            //上次的花费 + 本次话费  = 总话费
            lastTotalFee = lastTotalFee.add(totalFee);
            //保存本次花费 为下次计算做准备
            chargingData.setTotalFee(lastTotalFee);

            //获取上次的充电度数
            Float lastChargingCapacity = chargingData.getChargingCapacity();
            //实际充电度数 = 本次充电度数 - 上次充电度数
            Float realChargingCapacity = chargingCapacity - lastChargingCapacity;
            log.debug("本次实际充电度数:{}",realChargingCapacity);
            //保存本次充电度数 为下次计算做准备
            chargingData.setChargingCapacity(chargingCapacity);*/

            //保存到map中 为下次同步做准备 ???
            //chargingDataMap.put(orderNo,chargingData);

        }
        return chargingData;
    }

    private CostRulePO getCostRule(Integer currentHour,Integer stationId,Integer gunType){
        CostRulePO costRulePO = getMatchCostRuleByGunTypeAndTime(
                stationId,
                gunType,
                currentHour);
        log.debug("通过场站id:{},枪类型:{},时间:{},匹配到计价规则:{}",
                stationId,gunType,currentHour,costRulePO);
        return costRulePO;
    }


    @Override
    public ChargingProcessVO calculate(ChargingProcessParam chargingProcessParam) {
        Integer currentHour = getCurrentHour();
        log.debug("获取当前时间小时数:{}",currentHour);
        //获取计价规则
        CostRulePO costRulePO = getCostRule(currentHour,chargingProcessParam.getStationId(),
                chargingProcessParam.getGunType());
        if (costRulePO != null) {
            //获取设备同步的充电度数
            Float chargingCapacity = chargingProcessParam.getChargingCapacity();
            //ChargingData chargingData = getChargingDataByOrderNo(chargingProcessParam.getOrderNo());
            ChargingDataPO chargingData =
                    getChargingDataByOrderNoFromES(
                            chargingProcessParam.getOrderNo(),
                            currentHour,
                            costRulePO,
                            chargingCapacity,
                            chargingProcessParam.getUserId(),
                            chargingProcessParam.getGunId());
            log.debug("通过订单号:{},获取充电数据:{}",chargingProcessParam.getOrderNo(),
                    chargingData);
           /* //判断是否是第一次同步数据
            if (chargingData.getCount() == 1) {
                //第一次同步
                firstCost2ChargingData(chargingData,
                        chargingCapacity,powerFee,
                        currentHour,ruleName);
            }else {
                //第N次同步
                lastCost2ChargingData(chargingData,
                        chargingCapacity,powerFee,
                        currentHour,ruleName);
            }*/
            /**
             * 代码重构 理解 把不变的和变化的进行分离
             * first 和 last 基本逻辑是一样的
             * 1 获取实际充电度数
             * 2 计算价格
             * 3 保存度数
             * 4 保存花费
             * 区别
             * 1 充电度数的计算
             * 2 总花费的计算
             * 把不同的逻辑或者说变化的逻辑 抽取出来 抽到ChargingData中
             * 让第一次的计算和第N的计算 合并 复用
             */
            //cost2ChargingData(chargingData,chargingCapacity,powerFee,currentHour,ruleName);
            return buildChargingProcessVO(chargingData);
        }else {
            log.error("故障，计价规则为空，进行系统告警，业务停止");
        }
        return null;
    }

    /**
     * 通过充电数据 构建 计价服务 价格计算的出参
     * @param chargingData
     * @return
     */
    private ChargingProcessVO buildChargingProcessVO(ChargingDataPO chargingData) {
        ChargingProcessVO chargingProcessVO = new ChargingProcessVO();
        chargingProcessVO.setOrderNo(chargingData.getOrderNo());
        chargingProcessVO.setTotalFee(chargingData.getTotalFee());
        chargingProcessVO.setChargingCapacity(chargingData.getChargingCapacity());
        chargingProcessVO.setUserId(chargingData.getUserId());
        //如果参数过多，可以通过BeanUtils工具类进行拷贝  copy
        //为啥能 copy 属性值 ?
        //BeanUtils.copyProperties 原理
        //1 获取源 source chargingData.class 获取getter readMethod 读方法
        //2 获取目标 target ChargingProcessVO.class 获取setter writeMethod 写方法
        //3 循环遍历 readMethod 读方法
        //4 判断 读方法 和 写方法 的访问修饰符 如果不是 public setAccessible(true) 强制访问
        //5 通过 source的读方法 读取source的值 Object value = readMethod.invoke(source);
        //6 通过 writeMethod 写方法 把值写入到target中 writeMethod.invoke(target, value);
        // 属性名 必须一致才能拷贝 , 属性类型  会自动转换 能转换成功
        BeanUtils.copyProperties(chargingData,chargingProcessVO);

        //todo 自己实现 去 用时间的API去实现
        //充电总时长的计算  充电的结束时间 - 充电的开始时间
        //设备第一次同步的时候 记录下时间
        //充电完成后 获取当前时间 - 记录的开始时间 = 充电花费的总时长
        //chargingProcessVO.setChargingTime();

        return chargingProcessVO;

    }


    /**
     * 计算价格
     * @param chargingData
     * @param chargingCapacity
     * @param powerFee
     * @param currentHour
     * @param ruleName
     */
    private void cost2ChargingData(ChargingDataPO chargingData,
                                   Float chargingCapacity,BigDecimal powerFee,
                                   Integer currentHour,String ruleName){


    }

    /**
     * 2 two to
     * 计算第一次花费后 将数据保存到chargingData
     *  第一次
     *      本次花费  = 计价规则的电费 * 充电度数
     *      总花费 = 本次花费
     */
    private void firstCost2ChargingData(ChargingData chargingData,
                                        Float chargingCapacity,
                                        BigDecimal powerFee,
                                        Integer currentHour,String ruleName){
        log.debug("第一次同步数据，计算价格");
        //计算价格 充电度数 * 每度电的价格
        BigDecimal cost = getCost(chargingCapacity, powerFee);
        //保存度数 到 chargingData 为了下一次计算 <<实际充电度数>> 做准备
        chargingData.setChargingCapacity(chargingCapacity);
        //保存 本次花费 到 chargingData 为了下一次计算 <<总花费>> 做准备
        chargingData.setTotalFee(cost);
        log.debug("第一次同步数据，" +
                "当前时间:{},计价规则名称:{}," +
                "充电度数:{},电费单价:{}," +
                "本次花费:{},总花费:{}",
                currentHour,ruleName,
                chargingCapacity,powerFee,
                cost,cost);
    }


    /**
     * 2 two to
     * 计算第N花费 将数据保存到chargingData
     *  第N次
     *      本次花费  = 计价规则的电费 * （本次充电度数 - 上次充电度数 = 实际充电度数）
     *      总花费 = 本次花费 + 上次话费
     */
    private void lastCost2ChargingData(ChargingData chargingData,
                                        Float chargingCapacity,
                                        BigDecimal powerFee,
                                        Integer currentHour,String ruleName){
        log.debug("第N次同步数据，计算价格");
        //实际充电度数 = 本次充电度数 - 上次充电度数
        Float realChargingCapacity = chargingCapacity -
                chargingData.getChargingCapacity();
        //计算价格 充电度数 * 每度电的价格
        BigDecimal cost = getCost(realChargingCapacity, powerFee);
        //保存度数 到 chargingData 为了下一次计算 <<实际充电度数>> 做准备
        chargingData.setChargingCapacity(chargingCapacity);

        //计算花费
        //获取上次花费
        BigDecimal lastTotalFee = chargingData.getTotalFee();
        //总花费 = 本次花费 + 上次话费
        BigDecimal totalFee = lastTotalFee.add(cost);
        //保存 本次花费 到 chargingData 为了下一次计算 <<总花费>> 做准备
        chargingData.setTotalFee(totalFee);
        log.debug("第N次同步数据，" +
                        "当前时间:{},计价规则名称:{}," +
                        "充电度数:{},电费单价:{}," +
                        "本次花费:{},总花费:{}",
                currentHour,ruleName,
                chargingCapacity,powerFee,
                cost,totalFee);
    }



    /**
     * 计算价格
     * @param chargingCapacity
     * @param powerFee
     * @return
     */
    private BigDecimal getCost(Float chargingCapacity,BigDecimal powerFee) {
        //1>通过构造器 进行类型转换
        //Float 转换为 BigDecimal
        //BigDecimal bigDecimal = new BigDecimal(chargingCapacity);
        //基本类型的数字转换为 包装类型的 Integer
        //Integer i = new Integer(1);
        //String 转换为 包装类型的 String
        //String s = new String("1");

        //2>通过方法 进行类型转换 valueOf
        //通过方法 valueOf 转换 float 转为 String
        //String s = String.valueOf(chargingCapacity);
        //Float 转换为 BigDecimal
        BigDecimal bigDecimal = BigDecimal.valueOf(chargingCapacity);
        //BigDecimal add 加计算  multiply 乘法
        return bigDecimal.multiply(powerFee);
    }

    /**
     * 获取当前时间的小时
     * @return
     */
    private Integer getCurrentHour(){
        //@Deprecated
        // Date date = new Date();
        //date.getDate()
        //joda-time 时间的库
        //LocalDateTime java.time
        LocalDateTime localDateTime = LocalDateTime.now();
        return localDateTime.getHour();
    }


    private CostRulePO getMatchCostRuleByGunTypeAndTime(Integer stationId,
                                                        Integer gunType,
                                                        Integer hour) {
        log.debug("通过场站id获取计价规则列表-入参:{}",stationId);
        long start = System.currentTimeMillis();
        List<CostRulePO> costRulePOS = getCostRuleByStationId(stationId);
        long end = System.currentTimeMillis();
        long costTime = end - start;
        log.debug("通过场站id获取计价规则列表-入参:{},出参:{},消耗时间:{}",stationId,costRulePOS,costTime);

        if (!CollectionUtils.isEmpty(costRulePOS)) {


            /*costRulePOS.forEach(costRulePO -> {
                //具体的匹配逻辑
            });*/

      /*      Optional<CostRulePO> first = costRulePOS.stream().filter(costRulePO ->
                    costRulePO.getGunType().equals(gunType)
                            && hour >= costRulePO.getStartTime()
                            && hour < costRulePO.getEndTime()
            ).findFirst();
            if (first.isPresent()) { //return value != null;
                return first.get();
            }
*/



            for (CostRulePO costRulePO : costRulePOS) {
                //获取计价规则的枪类型
                Integer costRuleGunType = costRulePO.getGunType();
                //判断入参的枪类型和计价规则的枪类型是否一致
                if (costRuleGunType.equals(gunType)){
                    //获取计价规则开始时间
                    Integer startTime = costRulePO.getStartTime();
                    //获取计价规则结束时间
                    Integer endTime = costRulePO.getEndTime();
                    /**
                     * 当前时间 17:04   获取到小时数 17
                     * 找到真实的计价规则 通过人肉的方式 去判断 当前时间属于哪个时间段
                     * 8,12
                     * 14,18
                     * 18,24
                     * 12,14
                     * 0,8
                     *
                     * 17在 14 和 18 之间
                     * 17 > 14 并且 17 < 18
                     * hour > startTime && hour < endTime
                     *
                     * 边界值
                     * 如果当前时间是 8 改如何处理
                     * 如果当前时间是 12 改如何处理
                     *
                     * 如果当前时间是 8  是 大于 还是 大于等于
                     * 如果当前时间是 12  是 小于 还是 小于于等于
                     * 左闭右开
                     * 左开右闭
                     */
                     if (hour >= startTime && hour < endTime) {
                         return costRulePO;
                     }
                }
            }
        }
        //循环完了，还没有找到匹配的，返回null，表示没有匹配到计价规则
        return null;
    }

    @Autowired
    private CostRuleCacheRepository costRuleCacheRepository;

    /**
     * 通过场站id查询 当前场站所有的计价规则
     * getCostRuleByStationId 目前是通过数据库查询
     * 目标 是 通过缓存 查询 提高性能
     * 从 缓存查询 的前提是 缓存中得有数据
     * 缓存中的数据 什么时候有？
     *
     * 1 <<旁路缓存>> cacheAside  不提前准备，用的时候再说 懒汉
     *  1.1 先从Redis查询
     *  1.2 判断 Redis是否有数据
     *  1.3 有 返回
     *  1.4 没有 从数据库查询
     *  1.5 将查询到的数据保存到Redis中 返回
     *
     *  优点 启动的时间快
     *  缺点 第一个用户的访问速度慢，后续的就快了
     *
     * 2 <<缓存预热>>  提前准备好，用的时候直接用
     * 项目启动的时候
     * 从数据库 把计价规则 查询出来，保存到Redis中
     * 后续业务 查询计价规则的时候 直接从Redis中获取 不从数据库查
     *
     * 优点 不影响用户 第一个用户的访问速度也很快
     * 缺点 启动的时间 变长
     *
     * @param stationId
     * @return
     */
    private List<CostRulePO> getCostRuleByStationId(Integer stationId) {
        log.debug("通过场站id:{},从Redis获取计价规则列表",stationId);
        List<CostRulePO> costRulePOS =
                costRuleCacheRepository.getCostRuleByStationId(stationId);
        log.debug("通过场站id:{},从Redis获取计价规则列表:{}",stationId,costRulePOS);
        //判断Redis是否有数据
        if (!CollectionUtils.isEmpty(costRulePOS)) {
            log.debug("通过场站id:{},从Redis获取计价规则列表<<Redis有数据>>,直接返回",stationId);
            return costRulePOS;
        }else {
            log.debug("通过场站id:{},从Redis获取计价规则列表<<Redis没有数据>>,从数据库查询");
            List<CostRulePO> dbCostRules =
                    costRepository.getCostRulesByStationId(stationId);
            if (!CollectionUtils.isEmpty(dbCostRules)) {
                log.debug("通过场站id:{},从Redis获取计价规则列表<<Redis没有数据>>,从数据库查询<<数据库有数据>>,保存到Redis中");
                costRuleCacheRepository.saveCostRules(stationId,dbCostRules);
                return dbCostRules;
            }
        }
        return Collections.emptyList();
    }
}
